#include "sdk.h"
#include <wx/bitmap.h>
#include <wx/dc.h>
#include <wx/dcbuffer.h>
#include <wx/settings.h>
#include "byocbtris.h"
#include "byogame.h"

#define LOCK_START() static bool _lock = false; if ( _lock ) return; _lock = true;
#define LOCK_END() _lock = false;

namespace {
  const int Chunks[][16] = {
    {
      1, 1, 1, 1,
      0, 0, 0, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
    {
      1, 1, 0, 0,

      1, 1, 0, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
    {
      1, 1, 1, 0,
      1, 0, 0, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
    {
      1, 1, 1, 0,
      0, 0, 1, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
    {
      0, 1, 1, 0,
      1, 1, 0, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
    {
      1, 1, 0, 0,
      0, 1, 1, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
    {
      1, 1, 1, 0,
      0, 1, 0, 0,
      0, 0, 0, 0,
      0, 0, 0, 0
    },
  };

  const int ChunkTypes = sizeof( Chunks ) / sizeof( Chunks[0] );
  const int SpeedTimerId = wxNewId();
  const int LeftRightTimerId = wxNewId();
  const int UpTimerId = wxNewId();
  const int DownTimerId = wxNewId();
}

BEGIN_EVENT_TABLE( byoCBTris, GameBase )
  EVT_PAINT( byoCBTris::OnPaint )
  EVT_KEY_DOWN( byoCBTris::OnKeyDown )
  EVT_KEY_UP( byoCBTris::OnKeyUp )
  EVT_TIMER( SpeedTimerId, byoCBTris::OnSpeedTimer )
  EVT_TIMER( LeftRightTimerId, byoCBTris::OnLeftRightTimer )
  EVT_TIMER( UpTimerId, byoCBTris::OnUpTimer )
  EVT_TIMER( DownTimerId, byoCBTris::OnDownTimer )
  EVT_KILL_FOCUS( byoCBTris::OnKillFocus )
  EVT_ERASE_BACKGROUND( byoCBTris::OnEraseBack )
END_EVENT_TABLE()

byoCBTris::byoCBTris( wxWindow* parent, const wxString& Name ):
  GameBase( parent, Name ),
  SpeedTimer( this, SpeedTimerId ),
  LeftRightTimer( this, LeftRightTimerId ),
  UpTimer( this, UpTimerId ),
  DownTimer( this, DownTimerId ),
  m_Level( 1 ),
  m_Score( 0 ),
  m_IsLeft( false ),
  m_IsRight( false ),
  m_IsUp( false ),
  m_IsDown( false ),
  m_TotalRemovedLines( 0 ),
  m_Guidelines( false ) {
  m_Font = wxSystemSettings::GetFont( wxSYS_OEM_FIXED_FONT );
  LeftRightTimer.Start( 100 );
  UpTimer.Start( 1000 );
  DownTimer.Start( 20 );
  SetSpeed();
  memset( m_Content, 0, sizeof( m_Content ) );
  memset( m_CurrentChunk, 0, sizeof( m_CurrentChunk ) );
  memset( m_NextChunk, 0, sizeof( m_NextChunk ) );
  RandomizeChunk( m_NextChunk );
  GenerateNewChunk();
  RecalculateSizeHints( bricksHoriz + 2 * bricksMargin, bricksVert + 1 );
}

void byoCBTris::OnPaint( wxPaintEvent& ) {
  wxSize size = GetClientSize();
  wxBitmap buffer( wxImage( size.GetWidth(), size.GetHeight() ) );
  wxBufferedPaintDC DC( this, buffer );
  DrawBrickField( &DC );
  DrawCurrentChunk( &DC );
  DrawNextChunk( &DC );
  DrawStats( &DC );
}

void byoCBTris::OnKeyDown( wxKeyEvent& event ) {
  if( event.GetKeyCode() == 'p' || event.GetKeyCode() == 'P' ) {
    SetPause( !IsPaused() );
    Refresh();
  }
  if( IsPaused() ) {
    return;
  }
  if( event.GetKeyCode() == WXK_LEFT && !m_IsLeft ) {
    m_IsLeft = true;
    StartTimerNow( LeftRightTimer );
  }
  if( event.GetKeyCode() == WXK_RIGHT && !m_IsRight ) {
    m_IsRight = true;
    StartTimerNow( LeftRightTimer );
  }
  if( event.GetKeyCode() == WXK_UP && !m_IsUp ) {
    m_IsUp = true;
    StartTimerNow( UpTimer );
  }
  if( event.GetKeyCode() == WXK_DOWN && !m_IsDown ) {
    m_IsDown = true;
    StartTimerNow( DownTimer );
  }
  if( event.GetKeyCode() == 'g' || event.GetKeyCode() == 'G' ) {
    m_Guidelines = !m_Guidelines;
  }
}

void byoCBTris::OnKeyUp( wxKeyEvent& event ) {
  if( event.GetKeyCode() == WXK_LEFT ) {
    m_IsLeft = false;
  }
  if( event.GetKeyCode() == WXK_RIGHT ) {
    m_IsRight = false;
  }
  if( event.GetKeyCode() == WXK_UP ) {
    m_IsUp = false;
  }
  if( event.GetKeyCode() == WXK_DOWN ) {
    m_IsDown = false;
  }
}

void byoCBTris::OnLeftRightTimer( wxTimerEvent& ) {
  if( IsPaused() ) {
    return;
  }
  LOCK_START();
  UpdateChunkPosLeftRight();
  Refresh();
  LOCK_END();
}

void byoCBTris::OnUpTimer( wxTimerEvent& ) {
  if( IsPaused() ) {
    return;
  }
  LOCK_START();
  UpdateChunkPosUp();
  Refresh();
  LOCK_END();
}

void byoCBTris::OnDownTimer( wxTimerEvent& ) {
  if( IsPaused() ) {
    return;
  }
  LOCK_START();
  UpdateChunkPosDown();
  Refresh();
  LOCK_END();
}

void byoCBTris::OnSpeedTimer( wxTimerEvent& ) {
  if( IsPaused() ) {
    return;
  }
  LOCK_START();
  if( !ChunkDown() ) {
    RemoveFullLines();
    if( !GenerateNewChunk() ) {
      GameOver();
    }
  }
  Refresh();
  LOCK_END();
}

void byoCBTris::OnEraseBack( wxEraseEvent& ) {
}

void byoCBTris::DrawBrickField( wxDC* DC ) {
  static const wxColour Border( 0x40, 0x40, 0x40 );
  for( int x = 0; x < bricksHoriz; x++ ) {
    for( int y = 0; y < bricksVert; y++ ) {
      if( m_Content[x][y] ) {
        DrawBrick( DC, bricksMargin + x, y, GetColour( m_Content[x][y] ) );
      }
    }
  }
  for( int y = 0; y < bricksVert; y++ ) {
    DrawBrick( DC, bricksMargin - 1, y, Border );
    DrawBrick( DC, bricksMargin + bricksHoriz, y, Border );
  }
  for( int x = bricksMargin - 1; x <= bricksMargin + bricksHoriz; x++ ) {
    DrawBrick( DC, x, bricksVert, Border );
  }
  static const wxColour Guideline( 0x70, 0x70, 0x70 );
  if( m_Guidelines ) {
    DrawGuidelines( DC, bricksMargin, bricksHoriz, bricksVert, Guideline );
  }
}

void byoCBTris::DrawCurrentChunk( wxDC* DC ) {
  int* Ptr = m_CurrentChunk;
  for( int y = 0; y < 4; y++ ) {
    for( int x = 0; x < 4; x++ ) {
      if( *Ptr ) {
        DrawBrick( DC, x + m_ChunkPosX + bricksMargin, y + m_ChunkPosY, GetColour( *Ptr ) );
      }
      Ptr++;
    }
  }
}

void byoCBTris::DrawNextChunk( wxDC* DC ) {
  int* Ptr = m_NextChunk;
  for( int y = 0; y < 4; y++ ) {
    for( int x = 0; x < 4; x++ ) {
      if( *Ptr ) {
        DrawBrick( DC, bricksMargin - 5 + x, bricksVert - 5 + y, GetColour( *Ptr ) );
      }
      Ptr++;
    }
  }
}

void byoCBTris::RandomizeChunk( ChunkConfig& chunk, int color ) {
  if( color < 1 || color > 6 ) {
    color = ( int )( ( rand() * 6.0 / RAND_MAX ) + 1 );
  }
  if( color < 1 ) {
    color = 1;
  }
  if( color > 6 ) {
    color = 6;
  }
  int chunkNumber = ( int )( ( rand() * ( double )ChunkTypes ) / RAND_MAX );
  if( chunkNumber < 0 ) {
    chunkNumber = 0;
  }
  if( chunkNumber >= ChunkTypes ) {
    chunkNumber = ChunkTypes - 1;
  }
  color = chunkNumber + 1;
  for( int i = 0; i < 16; i++ ) {
    chunk[i] = ( Chunks[chunkNumber] )[i] * color;
  }
  for( int i = ( int )( rand() * 4.0 / RAND_MAX ); i-- > 0; ) {
    ChunkConfig newChunk;
    RotateChunkLeft( m_NextChunk, newChunk );
    memcpy( m_NextChunk, newChunk, sizeof( newChunk ) );
  }
}

void byoCBTris::RotateChunkLeft( const ChunkConfig& chunk, ChunkConfig& newChunk ) {
  for( int y = 0; y < 4; y++ ) {
    for( int x = 0; x < 4; x++ ) {
      newChunk[x + 4 * y] = chunk[( 3 - x ) * 4 + y];
    }
  }
  AlignChunk( newChunk );
}

void byoCBTris::RotateChunkRight( const ChunkConfig& chunk, ChunkConfig& newChunk ) {
  for( int y = 0; y < 4; y++ ) {
    for( int x = 0; x < 4; x++ ) {
      newChunk[x + 4 * y] = chunk[x * 4 + 3 - y];
    }
  }
  AlignChunk( newChunk );
}

void byoCBTris::AlignChunk( ChunkConfig& chunk ) {
  int rowsShift = 0;
  while( rowsShift < 4 ) {
    bool isEmpty = true;
    for( int x = 0; x < 4; x++ ) {
      if( chunk[rowsShift * 4 + x] != 0 ) {
        isEmpty = false;
        break;
      }
    }
    if( !isEmpty ) {
      break;
    }
    rowsShift++;
  }
  int colsShift = 0;
  while( colsShift < 4 ) {
    bool isEmpty = true;
    for( int y = 0; y < 4; y++ ) {
      if( chunk[colsShift + y * 4] != 0 ) {
        isEmpty = false;
        break;
      }
    }
    if( !isEmpty ) {
      break;
    }
    colsShift++;
  }
  if( !colsShift && !rowsShift ) {
    return;
  }
  ChunkConfig newChunk;
  memset( newChunk, 0, sizeof( newChunk ) );
  for( int y = rowsShift, y2 = 0; y < 4; y++, y2++ ) {
    for( int x = colsShift, x2 = 0; x < 4; x++, x2++ ) {
      newChunk[x2 + y2 * 4] = chunk[x + y * 4];
    }
  }
  memcpy( chunk, newChunk, sizeof( chunk ) );
}

bool byoCBTris::CheckChunkColision( const ChunkConfig& chunk, int posX, int posY ) {
  for( int y = 0; y < 4; y++ ) {
    for( int x = 0; x < 4; x++ ) {
      if( chunk[x + 4 * y] ) {
        int curX = posX + x;
        int curY = posY + y;
        if( curX < 0 ) {
          return true;
        }
        if( curX >= bricksHoriz ) {
          return true;
        }
        if( curY < 0 ) {
          return true;
        }
        if( curY >= bricksVert ) {
          return true;
        }
        if( m_Content[curX][curY] ) {
          return true;
        }
      }
    }
  }
  return false;
}

void byoCBTris::SetSpeed() {
  if( m_Level < 10 ) {
    SpeedTimer.Start( ( 10 - m_Level ) * 100 );
  } else {
    SpeedTimer.Start( 100 );
  }
}

bool byoCBTris::ChunkDown() {
  if( CheckChunkColision( m_CurrentChunk, m_ChunkPosX, m_ChunkPosY + 1 ) ) {
    for( int y = 0; y < 4; y++ )
      for( int x = 0; x < 4; x++ )
        if( m_CurrentChunk[x + 4 * y] ) {
          m_Content[x + m_ChunkPosX][y + m_ChunkPosY] = m_CurrentChunk[x + 4 * y];
        }
    m_Score += GetScoreScale();
    return false;
  }
  m_ChunkPosY++;
  return true;
}

void byoCBTris::RemoveFullLines() {
  int count = 0;
  int checkY = bricksVert - 1;
  int copyToY = bricksVert - 1;
  while( checkY >= 0 ) {
    bool isFull = true;
    for( int x = 0; x < bricksHoriz; x++ )
      if( !m_Content[x][checkY] ) {
        isFull = false;
      }
    if( isFull ) {
      count++;
    } else {
      if( checkY != copyToY ) {
        for( int x = 0; x < bricksHoriz; x++ ) {
          m_Content[x][copyToY] = m_Content[x][checkY];
        }
      }
      copyToY--;
    }
    checkY--;
  }
  while( copyToY >= 0 ) {
    for( int x = 0; x < bricksHoriz; x++ ) {
      m_Content[x][copyToY] = 0;
    }
    copyToY--;
  }
  m_Score += 10 * count * count * GetScoreScale();
  AddRemovedLines( count );
}

bool byoCBTris::GenerateNewChunk() {
  memcpy( m_CurrentChunk, m_NextChunk, sizeof( m_CurrentChunk ) );
  RandomizeChunk( m_NextChunk );
  m_ChunkPosY = 0;
  m_ChunkPosX = ( bricksHoriz - 4 ) / 2;
  return !CheckChunkColision( m_CurrentChunk, m_ChunkPosX, m_ChunkPosY );
}

void byoCBTris::GameOver() {
  Refresh();
  SpeedTimer.Stop();
  LeftRightTimer.Stop();
  DownTimer.Stop();
  UpTimer.Stop();
  SetPause( true );
  wxMessageBox( _( "Game over." ) );
}

void byoCBTris::UpdateChunkPosLeftRight() {
  if( m_IsLeft && !m_IsRight ) {
    if( !CheckChunkColision( m_CurrentChunk, m_ChunkPosX - 1, m_ChunkPosY ) ) {
      m_ChunkPosX--;
    }
  }
  if( !m_IsLeft && m_IsRight ) {
    if( !CheckChunkColision( m_CurrentChunk, m_ChunkPosX + 1, m_ChunkPosY ) ) {
      m_ChunkPosX++;
    }
  }
}

void byoCBTris::UpdateChunkPosUp() {
  bool IsShift = ::wxGetKeyState( WXK_SHIFT );
  if( m_IsUp ) {
    ChunkConfig newChunk;
    if( IsShift ) {
      RotateChunkLeft( m_CurrentChunk, newChunk );
    } else
    { RotateChunkRight( m_CurrentChunk, newChunk ); }
    if( !CheckChunkColision( newChunk, m_ChunkPosX, m_ChunkPosY ) ) {
      memcpy( m_CurrentChunk, newChunk, sizeof( newChunk ) );
    } else if( !CheckChunkColision( newChunk, m_ChunkPosX - 1, m_ChunkPosY ) ) {
      memcpy( m_CurrentChunk, newChunk, sizeof( newChunk ) );
      m_ChunkPosX--;
    } else if( !CheckChunkColision( newChunk, m_ChunkPosX + 1, m_ChunkPosY ) ) {
      memcpy( m_CurrentChunk, newChunk, sizeof( newChunk ) );
      m_ChunkPosX++;
    }
  }
}

void byoCBTris::UpdateChunkPosDown() {
  if( m_IsDown ) {
    if( CheckChunkColision( m_CurrentChunk, m_ChunkPosX, m_ChunkPosY + 1 ) ) {
      StartTimerNow( SpeedTimer );
    } else {
      m_ChunkPosY++;
      SpeedTimer.Start();
    }
  }
}

void byoCBTris::DrawStats( wxDC* DC ) {
  DC->SetTextForeground( *wxWHITE );
  DC->SetTextBackground( *wxBLACK );
  DC->SetFont( m_Font );
  wxString Line1 = wxString::Format( _( "Score: %d" ), m_Score );
  wxString Line2 = wxString::Format( _( "Level: %d" ), m_Level );
  wxString Line3 = GetBackToWorkString();
  DC->DrawText( Line1, 5, 5 );
  int xs, ys;
  DC->GetTextExtent( Line1, &xs, &ys );
  DC->DrawText( Line2, 5, 5 + 2 * ys );
  DC->DrawText( Line3, 5, 5 + 6 * ys );
  if( IsPaused() ) {
    DC->DrawText( _( "Paused" ), 5, 5 + 4 * ys );
  }
}

void byoCBTris::AddRemovedLines( int removed ) {
  m_TotalRemovedLines += removed;
  int shouldBeLevel = ( m_TotalRemovedLines / 20 ) + 1;
  if( shouldBeLevel != m_Level ) {
    m_Level = shouldBeLevel;
    SetSpeed();
  }
}

void byoCBTris::StartTimerNow( wxTimer& timer ) {
  timer.Notify();
  timer.Start();
}

int byoCBTris::GetScoreScale() const {
  return m_Guidelines ? 5 : 10;
}

void byoCBTris::OnKillFocus( wxFocusEvent& event ) {
  m_IsLeft = false;
  m_IsRight = false;
  m_IsUp = false;
  m_IsDown = false;
  event.Skip();
}

BYO_REGISTER_GAME( byoCBTris, "Tris" )
